#ifndef SINGLY_LIST_H
#define SINGLY_LIST_H

#include <cstddef>
#include <iostream>
#include <ostream>
#include <stdexcept>
using std::cout;
using std::endl;
using std::ostream;

template <class T>
class SinglyLinkedList {
    friend ostream& operator<<(ostream& out, SinglyLinkedList<T>& list) {
        if (list._size == 0) {
            out << "[EMPTY]";
            return out;
        }
        out << "[HEAD";
        for (Node* temp = list._head; temp != nullptr; temp = temp->_next) {
            out << " << " << temp->_value;
        }
        out << " << TAIL]";
        return out;
    }

    class Node {
       public:
        T _value;
        Node* _next;

        Node(T value = 0) : _value(value), _next(nullptr) {}

        Node(T value, Node* next) : _value(value), _next(next) {}
    };

   public:
    SinglyLinkedList<T>() : _head(nullptr), _tail(nullptr), _size(0) {}
    SinglyLinkedList<T>(SinglyLinkedList<T>& list) : _head(nullptr), _tail(nullptr), _size(0) {
        for (Node* curr = list._head; curr != nullptr; curr = curr->_next) {
            push_back(curr->_value);
        }
    }
    ~SinglyLinkedList<T>() { clear(); }

    void push_back(T value) {
        Node* newNode = new Node(value);
        if (empty()) {
            _head = _tail = newNode;
        } else {
            _tail->_next = newNode;
            _tail = newNode;
        }
        ++_size;
    }

    void pop_back() {
        if (empty()) {
            throw std::out_of_range("List is empty");
        }
        // Node* temp = _head;
        if (_head == _tail) {
            delete _head;
            _head = _tail = nullptr;
        } else {
            Node* temp = _head;
            while (temp->_next->_next != nullptr) {
                temp = temp->_next;
            }
            delete temp->_next;
            temp->_next = nullptr;
            _tail = temp;
        }
        --_size;
    }

    void push_front(T value) {
        Node* newNode = new Node(value);
        if (empty()) {
            _head = _tail = newNode;
        } else {
            newNode->_next = _head;
            _head = newNode;
        }
        ++_size;
    }

    void pop_front() {
        if (empty()) {
            throw std::out_of_range("List is empty");
        }
        if (_head == _tail) {
            delete _head;
            _head = _tail = nullptr;
        } else {
            Node* temp = _head;
            _head = _head->_next;
            delete temp;
        }
        --_size;
    }

    void insert(T value, size_t pos) {
        if (pos > _size) {
            throw std::out_of_range("list pos out of range");
        }
        if (pos == _size) {
            push_back(value);
            return;
        } else if (pos == 0) {
            push_front(value);
            return;
        }
        
        Node* curr = _head;
        for (int i = 0; i < pos - 1; ++i) {
            curr = curr->_next;
        }
        Node* newNode = new Node(value);
        newNode->_next = curr->_next;
        curr->_next = newNode;
        ++_size;
    }

    void erase(size_t pos) {
        if (pos >= _size) {
            throw std::out_of_range("list pos out of range");
        }
        if (pos == 0) {
            pop_front();
        }
        
        Node* curr = _head;
        for (int i = 0; i < pos - 1; ++i) {
            curr = curr->_next;
        }
        Node* temp = curr->_next;
        curr->_next = curr->_next->_next;
        delete temp;
        --_size;
    }

    bool empty() const { return _size == 0; }

    int size() const { return _size; }

    T& front() {
        if (empty()) {
            throw std::out_of_range("List is empty");
        }
        return _head->_value;
    }

    T& back() {
        if (empty()) {
            throw std::out_of_range("List is empty");
        }
        return _tail->_value;
    }

    void clear() {
        while (_head != nullptr) {
            Node* temp = _head;
            _head = _head->_next;
            delete temp;
        }
        _tail = nullptr;
        _size = 0;
    }

    using iterator = Node*;
    using const_iterator = const Node*;

    iterator begin() { return _head; }

    const_iterator begin() const { return _head; }

    SinglyLinkedList<T> operator=(SinglyLinkedList<T>& list) {
        if (this != &list) {
            this->clear();
            for (Node* curr = list._head; curr != nullptr; curr = curr->_next) {
                push_back(curr->_value);
            }
        }
        return *this;
    }

    static void print(SinglyLinkedList<T> list) { cout << list << endl; }

   private:
    Node* _head;
    Node* _tail;
    size_t _size;
};

#endif  // SINGLY_LIST_H